Skip to content

Update npm lockfile 3 ancestor traversal to properly detect dependency roots#1677

Open
FernandoRojo wants to merge 1 commit intomainfrom
users/ferojo/lockFileV3Roots
Open

Update npm lockfile 3 ancestor traversal to properly detect dependency roots#1677
FernandoRojo wants to merge 1 commit intomainfrom
users/ferojo/lockFileV3Roots

Conversation

@FernandoRojo
Copy link
Contributor

NpmLockfile3Detector had a path-resolution bug when resolving nested dependencies in lockfile v3.
The previous logic tried to reconstruct ancestor lookup paths from dependency-graph component IDs, inproperly traversing combinations of ancesters, instead of walking real lockfile package paths. That can produce invalid candidates and miss valid packages that are hoisted/placed under an ancestor path.

Root cause: ancestor traversal was based on component identity and an erroneous ancestor path determination., not the current lockfile path hierarchy.
Effect: a dependency can be reported as “not found” (or resolved to an incorrect fallback) even though it exists in packages.
Given:

node_modules/A
node_modules/A/node_modules/B
node_modules/A/node_modules/C ← C exists under ancestor A, not under B or root

If B declares dependency C, correct npm-style resolution is:

check
node_modules/A/node_modules/B/node_modules/C (missing)
then node_modules/A/node_modules/C (present, should resolve here)
then root node_modules/C (only if still missing)
The buggy approach could skip step 2 due to path reconstruction from component IDs, causing a false miss.

Fix summary

Resolution now walks from [currentPath] upward through real .../node_modules/... ancestors, checking each candidate path before top-level fallback.
This aligns lookup behavior with npm module resolution semantics and prevents false “missing dependency” outcomes in ancestor-hoisted layouts.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical bug in the npm lockfile v3 detector where dependency resolution failed to properly traverse ancestor paths when looking for hoisted dependencies. The previous implementation attempted to reconstruct ancestor paths from component IDs in the dependency graph, which could skip valid ancestor locations. The new implementation correctly walks the lockfile path hierarchy using string manipulation to find dependencies in ancestor node_modules directories before falling back to top-level resolution.

Changes:

  • Refactored EnqueueNestedDependencies method to use path-based ancestor traversal instead of dependency-graph-based traversal
  • Removed unused ISingleFileComponentRecorder parameter from EnqueueNestedDependencies
  • Added comprehensive test case to verify ancestor path resolution for nested dependencies

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Microsoft.ComponentDetection.Detectors/npm/NpmLockfile3Detector.cs Refactored dependency resolution to walk lockfile paths upward using LastIndexOf("/node_modules/") instead of reconstructing paths from component IDs; removed unused parameter
test/Microsoft.ComponentDetection.Detectors.Tests/NpmLockfile3DetectorTests.cs Added test case TestNpmDetector_ResolvesDependencyFromAncestorPathV3Async to verify correct resolution of dependencies from ancestor paths (A -> B -> C where both B and C are nested under A)

this.Logger.LogDebug("Found nested dependency {Dependency} in {AncestorNodeModulesPath}", dep.Key, ancestorNodeModulesPath);
queue.Enqueue((nestedPkg.Path, nestedPkg.Package, parent));
found = true;
continue;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The continue statement here is redundant because found is set to true and the while loop condition checks !found, which will cause the loop to exit naturally. The continue will skip the remaining iterations, but since the loop is already exiting, this doesn't add value. Consider removing this line for clarity.

Suggested change
continue;

Copilot uses AI. Check for mistakes.

public override string Id => "NpmLockfile3";

public override int Version => 3;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to bump detector version

@codecov
Copy link

codecov bot commented Feb 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.8%. Comparing base (e537f1d) to head (15a7c4e).

Additional details and impacted files
@@          Coverage Diff          @@
##            main   #1677   +/-   ##
=====================================
  Coverage   90.8%   90.8%           
=====================================
  Files        451     451           
  Lines      40144   40204   +60     
  Branches    2443    2443           
=====================================
+ Hits       36456   36517   +61     
  Misses      3188    3188           
+ Partials     500     499    -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

RushabhBhansali
RushabhBhansali previously approved these changes Mar 3, 2026
@RushabhBhansali RushabhBhansali dismissed their stale review March 3, 2026 17:05

Noticed failing verification tests

@RushabhBhansali
Copy link
Contributor

Have we investigated why are verification tests failing for Dotnet Components. Dotnet detector is not being touched in this PR.

Expected foundComponent to be True because The component for DotNet-10.0.102 netstandard1.4 unknown - DotNet was not present in the new manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.102 net40 unknown - DotNet was not present in the new manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.102 net45 unknown - DotNet was not present in the new manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for PipReport-certifi 2026.1.4 - pip was not present in the new manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.103 netstandard1.4 unknown - DotNet was not present in the old manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.103 net40 unknown - DotNet was not present in the old manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.103 net45 unknown - DotNet was not present in the old manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for NpmLockfile3-yallist 4.0.0 - Npm was not present in the old manifest file. Verify this is expected behavior before proceeding, but found False.

Copy link
Contributor

@RushabhBhansali RushabhBhansali left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we investigated why are verification tests failing for Dotnet Components. Dotnet detector is not being touched in this PR.

Expected foundComponent to be True because The component for DotNet-10.0.102 netstandard1.4 unknown - DotNet was not present in the new manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.102 net40 unknown - DotNet was not present in the new manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.102 net45 unknown - DotNet was not present in the new manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for PipReport-certifi 2026.1.4 - pip was not present in the new manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.103 netstandard1.4 unknown - DotNet was not present in the old manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.103 net40 unknown - DotNet was not present in the old manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for DotNet-10.0.103 net45 unknown - DotNet was not present in the old manifest file. Verify this is expected behavior before proceeding, but found False.
Expected foundComponent to be True because The component for NpmLockfile3-yallist 4.0.0 - Npm was not present in the old manifest file. Verify this is expected behavior before proceeding, but found False.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants